/*
 * Bundler.java
 * 
 * Created by demory on Oct 2, 2009, 10:11:33 PM
 * 
 * Copyright 2008 David D. Emory
 * 
 * This file is part of Five Points. See <http://www.fpdev.org> for
 * additional information regarding the project.
 * 
 * Five Points is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Five Points is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Five Points.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.fpdev.apps.cart.network;

import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.fpdev.util.FPUtil;

/**
 *
 * @author demory
 */
public class Bundler {

  // maps anchor pts to collections of angle-bundle pairs
  private Map<AnchorPoint, Map<Integer, Bundle>> bundles_;
  private Set<Comparison> comparisons_;

  public Bundler(CartNetwork network) {
    initBundles(network);
    //print();
    initComparisons(network);
    assignOffsets(network);
  }

  public void initBundles(CartNetwork network) {

    bundles_ = new HashMap<AnchorPoint, Map<Integer, Bundle>>();
    for(AnchorPoint pt : network.getAnchorPoints())
      bundles_.put(pt, new HashMap<Integer, Bundle>());

    for(Line line : network.getLines()) {

      //if(line.getID() != 2) continue;


      AnchorPoint from = line.startPoint(), to;
      for(Corridor c : line.getCorridors()) {
        to = (from != c.fPoint()) ? to = c.fPoint() : c.tPoint();
        
        int theta;
        Map<Integer, Bundle> bundlesAtPt;
        Bundle bundle;

        //System.out.println("corr "+c.getID());
        //System.out.println("from "+from.getID()+" to "+to.getID());
        // handle segment in the "forward" direction (starting with "from" point)
        if(c.isStraight()) theta = (int) Math.toDegrees(FPUtil.getTheta(to.getX()-from.getX(), to.getY()-from.getY()));
        else {
          Point2D.Double e = c.getElbow();
          theta = (int) Math.toDegrees(FPUtil.getTheta(e.x - from.getX(), e.y - from.getY()));
        }
        //System.out.println("fw theta = "+theta);
        bundlesAtPt = bundles_.get(from);
        if(!bundlesAtPt.containsKey(theta)) bundlesAtPt.put(theta, new Bundle(from, theta));
        bundle = bundlesAtPt.get(theta);
        bundle.addLine(new LineInfo(line, LineInfo.Direction.FORWARD, c));

        // handle segment in the "backward" direction (starting with "to" point)
        if(c.isStraight()) theta = (int) Math.toDegrees(FPUtil.getTheta(from.getX()-to.getX(), from.getY()-to.getY()));
        else {
          Point2D.Double e = c.getElbow();
          theta = (int) Math.toDegrees(FPUtil.getTheta(e.x - to.getX(), e.y - to.getY()));
        }
        //System.out.println("bw theta = "+theta);
        bundlesAtPt = bundles_.get(to);
        if(!bundlesAtPt.containsKey(theta)) bundlesAtPt.put(theta, new Bundle(to, theta));
        bundle = bundlesAtPt.get(theta);
        bundle.addLine(new LineInfo(line, LineInfo.Direction.BACKWARD, c));

        from = to;
      }
    }
  }

  public void initComparisons(CartNetwork network) {

    comparisons_ = new HashSet<Comparison>();

    // initialize diversion-based comparisons:
    for(Map.Entry<AnchorPoint, Map<Integer, Bundle>> entry : bundles_.entrySet()) {
      for(Bundle b : entry.getValue().values()) {
        LineInfo lines[] = (LineInfo[]) b.lines_.toArray(new LineInfo[b.lines_.size()]);
        for(int i = 0; i < lines.length; i++) {
          for(int j = i+1; j < lines.length; j++) {
            //initComparison(entry.getKey(), lines[i], lines[j]);
            initComparison(b, lines[i], lines[j]);
          }
        }
      }
    }

    //System.out.println(comparisons_.size()+" comparisons after diversion phase");

    // initialize containment-based comparisons:
    Line lines[] = (Line[]) network.getLines().toArray(new Line[network.getLines().size()]);
    for(int i = 0; i < lines.length; i++) {
      for(int j = i+1; j < lines.length; j++) {
        //if(lines[i].contains(lines[j])) {
        switch(lines[i].containsInOrder(lines[j])) {
          case 1:
            comparisons_.add(new Comparison(new LineInfo(lines[i], LineInfo.Direction.FORWARD, null), new LineInfo(lines[j], LineInfo.Direction.FORWARD, null)));
            comparisons_.add(new Comparison(new LineInfo(lines[j], LineInfo.Direction.BACKWARD, null), new LineInfo(lines[i], LineInfo.Direction.BACKWARD, null)));
            continue;
          case -1:
            comparisons_.add(new Comparison(new LineInfo(lines[j], LineInfo.Direction.FORWARD, null), new LineInfo(lines[i], LineInfo.Direction.BACKWARD, null)));
            comparisons_.add(new Comparison(new LineInfo(lines[i], LineInfo.Direction.FORWARD, null), new LineInfo(lines[j], LineInfo.Direction.BACKWARD, null)));
            continue;
        }
        switch(lines[j].containsInOrder(lines[i])) {
          case 1:
            comparisons_.add(new Comparison(new LineInfo(lines[j], LineInfo.Direction.FORWARD, null), new LineInfo(lines[i], LineInfo.Direction.FORWARD, null)));
            comparisons_.add(new Comparison(new LineInfo(lines[i], LineInfo.Direction.BACKWARD, null), new LineInfo(lines[j], LineInfo.Direction.BACKWARD, null)));
            continue;
          case -1:
            comparisons_.add(new Comparison(new LineInfo(lines[i], LineInfo.Direction.BACKWARD, null), new LineInfo(lines[j], LineInfo.Direction.FORWARD, null)));
            comparisons_.add(new Comparison(new LineInfo(lines[j], LineInfo.Direction.BACKWARD, null), new LineInfo(lines[i], LineInfo.Direction.FORWARD, null)));
            continue;
        }
      }
    }

    //System.out.println(comparisons_.size()+" comparisons after containment` phase");

    /*for(Comparison c: comparisons_) {
      System.out.println(" "+c.lesser_.toString()+ " < "+c.greater_.toString());
    }*/
  }

  //private void initComparison(AnchorPoint anchor, LineInfo line1, LineInfo line2) {
  private void initComparison(Bundle bundle, LineInfo line1, LineInfo line2) {
    AnchorPoint anchor = bundle.anchor_;
    AnchorPoint opp1 = line1.corr_.opposite(anchor);
    AnchorPoint opp2 = line2.corr_.opposite(anchor);

    Corridor adj1 = line1.line_.adjacent(line1.corr_, anchor);
    Corridor adj2 = line2.line_.adjacent(line2.corr_, anchor);

    // SPLIT BUNDLE COMPARISON
    if(adj1 != null && adj2 != null && adj1 != adj2) {
      //System.out.println("sb comp");

      Point2D.Double pt1 = adj1.isStraight() ?
        adj1.opposite(anchor).getPoint2D() :
        adj1.getElbow();

      Point2D.Double pt2 = adj2.isStraight() ?
        adj2.opposite(anchor).getPoint2D() :
        adj2.getElbow();

      int theta1 = (int) Math.toDegrees(FPUtil.getTheta(pt1.x - anchor.getX(), pt1.y - anchor.getY()));
      int theta2 = (int) Math.toDegrees(FPUtil.getTheta(pt2.x - anchor.getX(), pt2.y - anchor.getY()));

      int dt1 = theta1 - bundle.theta_;
      if(dt1 < 0) dt1 += 360;
      int dt2 = theta2 - bundle.theta_;
      if(dt2 < 0) dt2 += 360;

      if(dt1 < dt2) { // line1 bundled before line1
        //System.out.println(" 1 < 2");
        comparisons_.add(new Comparison(line1, line2));
        comparisons_.add(new Comparison(line2.getReverse(), line1.getReverse()));
      }
      else if(dt2 < dt1) { // line2 bundled before line1
        //System.out.println(" 2 < 1");
        comparisons_.add(new Comparison(line2, line1));
        comparisons_.add(new Comparison(line1.getReverse(), line2.getReverse()));
      }
    }

    // INLINE BUNDLE COMPARISON
    else if(opp1 != opp2) {
      Point2D.Double pt = null;

      if(line1.corr_.isStraight() && !line2.corr_.isStraight())
        pt = line2.corr_.getElbow();
      else if(!line1.corr_.isStraight() && line2.corr_.isStraight())
        pt = line1.corr_.getElbow();
      else if(!line1.corr_.isStraight() && !line2.corr_.isStraight()) {
        Point2D.Double e1 = line1.corr_.getElbow(), e2 = line2.corr_.getElbow();
        double dist1 = FPUtil.magnitude(anchor.getX(), anchor.getY(), e1.x, e1.y);
        double dist2 = FPUtil.magnitude(anchor.getX(), anchor.getY(), e2.x, e2.y);
        if(dist1 < dist2) pt = e1;
        else pt = e2;
      }
      else // both are straight
        return;

      /*System.out.println(" ("+opp1.getX()+", "+opp1.getY()+")");
      System.out.println(" ("+pt.x+", "+pt.y+")");
      System.out.println(" ("+opp2.getX()+", "+opp2.getY()+")");*/

      int ccw = Line2D.relativeCCW(opp1.getX(), opp1.getY(), pt.x, pt.y, opp2.getX(), opp2.getY());
      if(ccw == 1) { // a counterclockwise or "right" turn: line2 bundled before line1
        //System.out.println(" right turn, 2 < 1");
        comparisons_.add(new Comparison(line2, line1));
        comparisons_.add(new Comparison(line1.getReverse(), line2.getReverse()));
      }
      if(ccw == -1) { // a clockwise or "left" turn: line2 bundled before line1
        //System.out.println(" left turn, 1 < 2");
        comparisons_.add(new Comparison(line1, line2));
        comparisons_.add(new Comparison(line2.getReverse(), line1.getReverse()));
      }
    }
  }

  private Set<String> assignedOffsets_; // "[lineid]_[anchorid]_[theta]"

  public void assignOffsets(CartNetwork network) {
    for(Line line : network.getLines())
      line.clearOffsets();

    List<Bundle> sortedBundles = new LinkedList<Bundle>();//new AnchorBundleSizeComparator());
    assignedOffsets_ = new HashSet<String>();

    for(Map.Entry<AnchorPoint, Map<Integer, Bundle>> entry : bundles_.entrySet()) {
      for(Bundle b : entry.getValue().values()) {
        sortedBundles.add(b);
      }
    }
    Collections.sort(sortedBundles);

    //System.out.println("sortedBundles size="+sortedBundles.size());

    while(!sortedBundles.isEmpty()) {
      Bundle b = sortedBundles.get(sortedBundles.size()-1);
      if(b.size() == 1) break;

      //System.out.println("sorting bundle "+b.anchor_.getID()+":"+b.theta_+" size="+b.lines_.size());
      List<LineInfo> bundledLines = new LinkedList<LineInfo>();
      for(LineInfo li : b.lines_) bundledLines.add(li);
      Collections.sort(bundledLines, new BundledLineComparator());

      int offset = -b.getWidth()/2;// + bundledLines.get(0).line_.getStyle().getWidth()/2;
      //int offset = -((b.size()-1)*10)/2;
      for(LineInfo li : bundledLines) {
        offset += li.line_.getStyle().getWidth()/2;

        assignOffset(li.line_, li.corr_, b.anchor_, b.theta_, offset);
        extendOffset(li.line_, li.corr_, b.anchor_, b.theta_, offset);

        Corridor oppCorr = li.line_.adjacent(li.corr_, b.anchor_);
        
        if(oppCorr != null && Math.abs(oppCorr.getAngle(b.anchor_)-b.theta_) == 180) {

          assignOffset(li.line_, oppCorr, b.anchor_, oppositeTheta(b.theta_), -offset);
          extendOffset(li.line_, oppCorr, b.anchor_, oppositeTheta(b.theta_), -offset);
        }


        offset += li.line_.getStyle().getWidth()/2; //10;
      }

      sortedBundles.remove(b);
    }
  }

  private void assignOffset(Line line, Corridor corr, AnchorPoint anchor, int theta, int offset) {
    String str = line.getID()+"_"+anchor.getID()+"_"+theta;
    if(assignedOffsets_.contains(str)) return;
    //System.out.println(" assigning offset "+offset+" to "+line.getName()+"_"+anchor.getID()+"_"+theta);

    Line.CorridorInfo ci = line.getCorridorInfo(corr);
    if(anchor == corr.fPoint()) ci.offsetFrom_ = offset;
    if(anchor == corr.tPoint()) ci.offsetTo_ = -offset;

    assignedOffsets_.add(str);
  }

  private void extendOffset(Line line, Corridor corr, AnchorPoint anchor, int theta, int offset) {

    if(!corr.isStraight()) return;

    while(corr.isStraight()) {
      anchor = corr.opposite(anchor);
      assignOffset(line, corr, anchor, oppositeTheta(theta), -offset);
      Corridor adj = line.adjacent(corr, anchor);
      if(adj == null || Math.abs(adj.getAngle(anchor)-corr.getAngle(anchor)) != 180) return;
      assignOffset(line, adj, anchor, theta, offset);
      corr = adj;
    }


  }

  public int oppositeTheta(int theta) {
    theta += 180;
    if(theta >= 360) theta -= 360;
    return theta;
  }

  public void print() {
    System.out.println("-- PRINTING BUNDLES --");
    for(AnchorPoint pt : bundles_.keySet()) {
      System.out.println("Point "+pt.getID());
      for(Map.Entry<Integer, Bundle> entry : bundles_.get(pt).entrySet()) {
        System.out.println("  Bundle at "+entry.getKey()+" degrees:");
        for(LineInfo line : entry.getValue().lines_) {
          System.out.println("    "+line.toString());
        }
      }
    }
    System.out.println("-- END BUNDLES --");
  }

  public static class LineInfo {

    public enum Direction { 
      FORWARD("fw"),
      BACKWARD("bw");

      private String disp_;

      Direction(String disp) {
        disp_ = disp;
      }

      @Override
      public String toString() {
        return disp_;
      }
    };

    Line line_;
    Direction dir_;
    Corridor corr_;

    /*public LineInfo(Line line, Direction dir) {
      this(line, dir, null);
    }*/

    public LineInfo(Line line, Direction dir, Corridor corr) {
      line_ = line;
      dir_ = dir;
      corr_ = corr;
    }

    public LineInfo getReverse() {

      return new LineInfo(line_, (dir_ == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD), corr_);
    }

    @Override
    public String toString() {
      return line_.getName() + "(#" + line_.getID() + ") " + dir_;
    }
  }

  public static class Bundle implements Comparable {

    private AnchorPoint anchor_;
    private int theta_;
    private Set<LineInfo> lines_;

    public Bundle(AnchorPoint anchor, int theta) {
      anchor_ = anchor;
      theta_ = theta;
      lines_ = new HashSet<LineInfo>();
    }

    public void addLine(LineInfo line) {
      lines_.add(line);
    }

    public int size() {
      return lines_.size();
    }

    public int compareTo(Object o) {
      return new Integer(lines_.size()).compareTo(((Bundle) o).size());
    }

    public int getWidth() {
      int width = 0;
      for(LineInfo li : lines_) {
        width += li.line_.getStyle().getWidth();
      }
      return width;
    }
  }


  public static class Comparison {

    LineInfo lesser_, greater_;

    public Comparison(LineInfo lesser, LineInfo greater) {
      lesser_ = lesser;
      greater_ = greater;
    }

    public boolean equals(Comparison c) {
      return lesser_.line_ == c.lesser_.line_ && lesser_.dir_ == c.lesser_.dir_ &&
             greater_.line_ == c.greater_.line_ && greater_.dir_ == c.greater_.dir_;
    }
  }

  public class BundledLineComparator implements Comparator<LineInfo> {

    public int compare(LineInfo o1, LineInfo o2) {
      //System.out.println("comparing "+o1.toString()+" and "+o2.toString());
      int ret = 0;
      Comparison lessThan = new Comparison(o1, o2); // assume o1 < o2
      Comparison greaterThan = new Comparison(o2, o1); // assume o1 > o2
      for(Comparison c : comparisons_) {
        if(c.equals(lessThan)) ret--;
        if(c.equals(greaterThan)) ret++;
      }
      //System.out.println("blc ret="+ret);
      return ret;
    }
  }
}
